Pro ASP.NET Core MVC2(第7版)翻译

第14章:配置应用程序

作者:Adam Freeman 翻译:陈广 日期:2018-9-12


配置这一主题似乎并不有趣,但它揭示了 MVC 应用程序是如何工作以及 HTTP 请求是如何处理的。抵制跳过本章的诱惑,花时间了解配置系统如何构建 MVC web 应用程序。它将为您理解接下来的章节提供坚实的基础。

在本章中,我将解释如何使用这些来配置 MVC 应用程序,并展示 MVC 如何基于 ASP.NET Core 平台提供的功能进行构建。表14-1为配置应用程序简述。

表 14-1:配置问答

问题 回答
它是什么? ProgramStartup类以及 JSON 文件用于配置应用程序是如何工作以及需要依赖哪些包的
它有什么用? 配置系统允许应用程序订制它们的环境以及管理它们的包依赖
它如何使用? 最重要的组件是Startup类,它用于创建服务(在整个应用程序中提供公共功能的对象)以及中间件组件(用于处理 HTTP 请求)
是否有什么缺陷或限制? 在复杂的应用程序中,配置变得难以管理。请参阅《处理复杂配置》一节中,处理这个问题的 ASP.NET 功能。
有没有其它选择? 没有,配置系统是 ASP.NET 的一个组成部分,也是建立MVC应用程序的手段。

表 14-2:章节摘要

问题 解决方法 清单
向应用程序添加功能 将 NuGet 包加进 csproj 文件 5-8
管理 ASP.NET 应用程序的初始化 使用Program 9-11
配置应用程序 使用Startup类的ConfigureServicesConfigure方法 12-13
创建通用功能 使用ConfigureServices方法创建服务 14-16
生成内容响应 创建内容生成中间件 17-19
防止请求遍历请求管道 创建短路中间件 20-21
在请求被其它中间件组件处理之前编辑它 创建请求编辑中间件 22-24
编辑已由其他中间件组件处理的响应 创建响应编辑中间件 25-26
设置 MVC 功能 使用UseMvcUseMvcWithDefaultRoute方法 27
为不同环境更改应用程序配置 使用宿主环境服务 28
处理应用程序错误 用开发或生产环境错误处理中间件 29-30
在开发期间管理多浏览器 使用浏览器链接 31
启用图像、JavaScript 文件和 CSS 文件 启用静态内容中间件 32
将配置数据与 C# 代码分离 创建外部配置源,如 JSON 文件 33-35
日志应用数据 使用 logging 中间件 36-38
为 Entity Framework Core 的使用准备依赖注入 禁用范围验证 39
配置 MVC 服务 使用选项功能 40
配置复杂应用程序 使用多个外部文件或类 41-45

准备示例项目

本章,我使用【空】模板创建了一个名为 ConfiguringApps 的新项目。本章稍后将配置应用程序,但在将要做出这些改变时,需要做一些基本的准备。

在本章中,我将使用 Bootstrap 对 HTML 内容进行样式化,因此我使用 LibMan 引入 Bootstrap,并生成如清单14-1所示的包。

清单 14-1:ConfiguringApps 文件夹下的 libman.json 文件,添加 Bootstrap

{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "libraries": [
    {
      "library": "twitter-bootstrap@4.1.3",
      "destination": "wwwroot/lib/twitter-bootstrap/"
    }
  ]
}

接下来,我创建了 Controllers 文件夹,并在里面添加了一个名为 HomeController.cs 的类文件,用于定义清单14-2所示的控制器。

清单 14-2:Controllers 文件夹下的 HomeController.cs 文件的内容

using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;

namespace ConfiguringApps.Controllers
{
    public class HomeController : Controller
    {
        public ViewResult Index() => View(new Dictionary<string, string>
        {
            ["Message"] = "This is the Index action"
        });
    }
}

我创建了一个 View/Home 文件夹,并添加了一个名为 Index.cshtml 的视图文件,内容如清单14-3所示。

清单 14-3:Views/Home 文件夹下的 Index.cshtml 文件的内容

@model Dictionary<string, string>
@{ Layout = null; }

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <link asp-href-include="lib/twitter-bootstrap/css/*.min.css" rel="stylesheet" />
    <title>Result</title>
</head>
<body class="p-1">
    <table class="table table-condensed table-bordered table-striped">
        @foreach (var kvp in Model)
        {
            <tr><th>@kvp.Key</th><td>@kvp.Value</td></tr>
        }
    </table>
</body>
</html>

视图内的link元素依赖于内置标签助手选择 Bootstrap CSS 文件。为启动标签助手,我使用了【Razor 视图导入】模板在 View 文件夹内创建了 _ViewImports.cshtml 文件,并添加了清单14-4所示的表达式。

清单 14-4:Views 文件夹下的 _ViewImports.cshtml 文件的内容

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

开始应用程序,你将看到如图14-1所示的消息。

图14-1 运行示例应用程序

配置项目

最重要的配置文件是<projectname>.csproj,它取代 ASP.NET Core 早期版本中使用的 project.json 文件。此文件在项目中的名称为 ConfiguringApps.csproj,被 Visual Studio 隐藏,想要访问,必须在【解决方案资源管理器】中右键单击项目,并在弹出菜单中选择【编辑 ConfiguringApps.csproj】。清单 14-5显示了 ConfiguringApps.csproj 文件的初始内容,它作为 Visual Studio 【空】项目模板的一部分被创建。

清单 14-5:ConfiguringApps 文件夹下的 ConfiguringApps.csproj 文件的内容

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp2.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <Folder Include="wwwroot\" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.App" />
  </ItemGroup>

</Project>

译者注:原文使用的是 .NET Core 2.0 的模板,我在清单里放的是 .NET Core 2.1 的模板。区别是 2.1 使用Microsoft.AspNetCore.App包取代了 2.0 中的Microsoft.AspNetCore.All

csproj 文件用于配置 MSBuild 工具,此工具用于生成 .NET 项目。配置使用 XML 元素实现,表14-3描述了默认配置文件中的元素。在之后的章节中我会使用其它配置元素,但表中元素已经足够开始 ASP.NET Core MVC 项目的开发。

表 14-3:csproj 文件的默认 XML 配置元素

元素 描述
project 这是根元素,它表示这是一个 MSBuild 配置文件。SDK 属性被设置为 Microsoft.NET.Sdk.Web,以提供构建项目所需的隐式包导入集。
PropertyGroup 此元素将相关配置属性分组,以向文件中添加结构
TargetFramework 此元素指定了构建过程所针对的 .NET 框架,并且必须在 PropertyGroup 元素中定义。默认值是netcoreapp2.1,针对的是.net core 2.1。
ItemGroup 此元素将相关配置项分组,以将结构添加到文件中。
Folder 这个元素告诉 MSBuild 如何处理项目中的文件夹。清单中的元素告诉 MSBuild 在发布应用程序时包含 wwwroot 文件夹。
PackageReference 此元素用于指定 NuGet 包的依赖项,该依赖项通过 Include 和 Version 属性来标识。 Microsoft.AspNetCore.All 包用于对提供了 ASP.NET Core 和 MVC 框架功能的所有单独包的访问。

将包添加至项目

csproj 文件最重要的角色是列出项目所依赖的包。当 Visual Studio 检测到 csproj 文件的更改时,它将检查包列表,下载任何新添加的内容,并删除不再需要的任何包。

随着 ASP.NET Core 2 的发布,ASP.NET Core MVC、MVC框架和 Entity Framework Core 所需的所有基本功能都包含在 Microsoft.AspNetCore.All 元包中,这是一个方便的特性,它避免了通过向项目中添加一个很长的 NuGet 包列表来开始新的开发工作。

即便如此,您仍然需要为第三方或高级功能添加 NuGet 软件包。向项目添加包有三个途径。第一个是选择【工具】➤【NuGet 包管理器】➤【管理解决方案的 NuGet 程序包】,它允许通过一个易于使用的界面来管理 NuGet 包。如果您是 .NET 开发新手,那么这是最好的方法,因为它减少了在选择包时出错的可能性。

以添加 System.Net.Http 包为例,它提供了对发出 HTTP 请求的支持(不是接收),您可以转到包管理器的【浏览】部分,按名称进行搜索,并查看可用版本的完整列表,包括任何预发布版本,如图14-2所示。

图14-2 使用 NuGet 包管理器添加包

选择所需的软件包和版本,选择软件包所需的项目,然后单击【安装】按钮。Visual Studio 将下载该包并更新 csproj 文件。

您也可以使用命令行添加包,尽管这要求您知道所需包的名称(以及理想的版本)。清单14-6显示了在 ConfiguringApps 文件夹中运行的命令,它会将 System.Net.Http 包添加到项目中的。

清单 14-6:将包添加至项目

dotnet add package System.Net.Http --version 4.3.3

NuGet 包管理器和dotnet add package命令都可以将PackageReference元素添加进 csproj 文件。如果你喜欢,也可以编辑配置文件通过手动编写PackageReference元素来添加包。这是最直接的方法,但需要注意避免输入错误的包名称或指定不存在的版本号。在清单14-7中,您可以看到在 csproj 文件添加的 System.Net.Http 包。

清单 14-7:ConfiguringApps 文件夹下的 ConfiguringApps.csproj 文件,添加包

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp2.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <Folder Include="wwwroot\" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.App" />
    <PackageReference Include="System.Net.Http" Version="4.3.3" />
  </ItemGroup>

</Project>

PackageReference元素的Include属性指定包名称,Version属性指定版本号。

在项目中添加工具包

虽然可以用不同的方式添加常规包,但有些包扩展了可以使用 dotnet 命令行工具执行的任务范围,这些包需要在 csproj 文件中使用不同类型的元素,所包含的功能可以直接被应用程序使用的包使用的是DotNetCliToolReference元素,而不是PackageReference元素。这些包只能通过直接编辑 csproj 文件才能添加到项目中。

清单14-8显示了附加的的包,它在本书第一部分中被用来使用dotnet ef命令创建和应用数据库迁移。

清单 14-8:ConfiguringApps 文件夹下的 ConfiguringApps.csprog 文件,添加工具包

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <Folder Include="wwwroot\" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
    <PackageReference Include="System.Net.Http" Version="4.3.2" />
    <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0 " />
  </ItemGroup>

</Project>

译者注:.NET Core 2.1 已自动包含Microsoft.EntityFrameworkCore.Tools.DotNet工具包,无需手动加入,此代码是 .NET Core 2.0 中的,如果使用的是 2.1 版本,可以忽略此小节。

当您向项目添加工具包时,您可以将DotNetCliToolReference元素包含在与普通PackageReference元素相同的ItemGroup中,如我在清单14-8中所做的那样,也可以创建一个单独的ItemGroup元素。当您将更改保存到 csproj 文件时,Visual Studio 将下载并安装这些包,并使用它们配置 dotnet 命令行工具。

理解 Program 类

Program类是在一个名为 Program.cs 的文件中定义的,它提供了运行应用程序的入口点,为 .NET 提供一个main方法,可以通过执行该方法来配置宿主环境,并为 ASP.NET Core 应用程序选择类以完成配置。Program类的默认内容足以让大多数项目启动和运行,清单14-9显示了 Visual Studio 添加到项目中的默认代码。

清单 14-9-1:ConfiguringApps 文件夹下的 Program.cs 文件,.NET 2.0 中的默认内容

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

namespace ConfiguringApps 
{
  public class Program 
  {
    public static void Main(string[] args) 
    {
      BuildWebHost(args).Run();
    }

    public static IWebHost BuildWebHost(string[] args) =>
      WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>()
        .Build();
  }
}

Main方法提供了所有 .NET 应用程序必须提供的入口点,以便运行时能够执行它们。Program类中的Main方法调用BuildWebHost方法,该方法负责配置 ASP.NET Core。

BuildWebHost方法使用由WebHost类定义的静态方法来配置 ASP.NET Core。随着 ASP.NET Core 2 的发布,使用CreateDefaultBuilder方法简化了配置,该方法配置 ASP.NET Core 所使用的设置可能适合大多数项目。UseStartup方法用来标识将提供应用程序指定配置的类;约定是使用一个名为Startup的类,我将在本章后面对此进行描述。Build方法处理所有配置设置,并创建一个实现了IWebHost接口的对象,该对象返回给Main方法,Main方法调用Run以开始处理 HTTP 请求。

译者注:以上为 .NET Core 2.0版本的Program类代码,在 2.1 版本中有了些许改动。下面列出的是 2.1 版本的Program类代码。

清单 14-9-2:ConfiguringApps 文件夹下的 Program.cs 文件,.NET 2.1 中的默认内容

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

namespace ConfiguringApps
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateWebHostBuilder(args).Build().Run();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>();
    }
}

对比两段代码,其实可以发现,本质上是没有改动的,只是 2.0 版本的Build方法在 2.1 版本被放到了Main方法中去执行。微软这样做肯定是有他的道理,可能是为了方便在Main方法可以更方便地做一些其它控制。

深入了解配置细节

CreateDefaultBuilder方法是一种快速启动 ASP.NET Core 配置的便利方法,但它确实隐藏了许多重要细节,如果需要更改应用程序的配置方式,这可能是一个问题。清单14-10用创建默认配置所需调用的单个语句替换CreateDefaultBuilder方法。

清单 14-10:ConfiguringApps 文件夹下的 Program.cs 文件,配置语句细节

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System.Reflection;

namespace ConfiguringApps
{
    public class Program
    {
        public static void Main(string[] args)
        {
            BuildWebHost(args).Run();
        }

        public static IWebHost BuildWebHost(string[] args)
        {
            return new WebHostBuilder()
              .UseKestrel()
              .UseContentRoot(Directory.GetCurrentDirectory())
              .ConfigureAppConfiguration((hostingContext, config) => {
                  var env = hostingContext.HostingEnvironment;
                  config.AddJsonFile("appsettings.json", optional: true,
                          reloadOnChange: true)
                      .AddJsonFile($"appsettings.{env.EnvironmentName}.json",
                          optional: true, reloadOnChange: true);

                  if (env.IsDevelopment())
                  {
                      var appAssembly =
                          Assembly.Load(new AssemblyName(env.ApplicationName));
                      if (appAssembly != null)
                      {
                          config.AddUserSecrets(appAssembly, optional: true);
                      }
                  }

                  config.AddEnvironmentVariables();
                  if (args != null)
                  {
                      config.AddCommandLine(args);
                  }
              })
              .ConfigureLogging((hostingContext, logging) => {
                  logging.AddConfiguration(
                      hostingContext.Configuration.GetSection("Logging"));
                  logging.AddConsole();
                  logging.AddDebug();
              })
              .UseIISIntegration()
              .UseDefaultServiceProvider((context, options) => {
                  options.ValidateScopes =
                  context.HostingEnvironment.IsDevelopment();
              })
              .UseStartup<Startup>()
              .Build();
        }
    }
}

表14-4列出了添加到BuildWebHost方法中的每一个配置方法,并提供了它们所做工作的简要说明。

表 14-4:默认 ASP.NET Core 配置方法

名称 描述
UseKestrel 此方法配置 Kestrel web 服务器,稍后在本节的《直接使用 Kestrel》中描述
UseContentRoot 此方法配置应用程序的根目录,用于加载配置文件和传递静态内容,如图像、JavaScript 和 CSS。
ConfigureAppConfiguration 此方法用于为应用程序准备配置数据,在本章稍后的《配置应用程序》这一节有描述
AddUserSecrets 此文件用于在代码文件之外存储敏感数据,如https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets所描述。这是一个有点尴尬的特性,我没有在这本书中使用。
ConfigureLogging 此方法用于为应用程序配置日志,本章后面的《配置日志》有描述。
UseIISIntegration 该方法启用 IIS 与 IIS 表达式的集成。
UseDefaultServiceProvider 此方法用于配置依赖注入,如《配置依赖项注入》这一节所述。
UseStartup 此方法指定将用于配置 ASP.NET 的类,如《理解 Startup 类》这一节所述。

在本章的后面,我将解释一些如清单14-10所示的更复杂的语句。现在,我将删除一些配置语句,以便只保留基本配置,如清单14-11所示。

清单 14-11:ConfiguringApps 文件夹下的 Program.cs 文件,简化配置

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System.Reflection;

namespace ConfiguringApps
{
    public class Program
    {
        public static void Main(string[] args)
        {
            BuildWebHost(args).Run();
        }
        public static IWebHost BuildWebHost(string[] args)
        {
            return new WebHostBuilder()
                .UseKestrel()
                .UseContentRoot(Directory.GetCurrentDirectory())
                .UseIISIntegration()
                .UseStartup<Startup>()
                .Build();
        }
    }
}

这些语句为大多数 ASP.NET Core MVC 应用程序提供了基本的配置,我将在解释它们所涉及的其他功能时添加相应语句。


直接使用 Kestrel

Kestrel是一个跨平台的 Web 服务器,设计用于运行 ASP.NET Core 应用程序。当您使用 IIS Express(它是 Visual Studio 提供的用于开发过程中使用的服务器)或完整版本的 IIS 运行 ASP.NET Core 应用程序时,会自动使用 IIS Express,后者一直是 .NET 应用程序的传统 web 平台。

如果需要,还可以直接运行 Kestrel,这意味着您可以在任何支持的平台上运行 ASP.NET Core MVC 应用程序,从而绕过 IIS 的 Windows 专用限制。使用 Kestrel 运行应用程序有两种方法。第一种方法是单击 Visual Studio 工具栏上IIS Express 按钮右侧的箭头,并选择与项目名称匹配的条目。这将打开一个新的命令提示符,并使用 Kestrel 运行应用程序。

通过打开自己的命令提示符、导航到包含应用程序配置文件的文件夹(包含 csproj 文件的文件夹),并运行以下命令,可以达到同样的效果:
dotnet run
默认情况下,Kestrel 服务器在端口5000上侦听的 HTTP 请求。如果项目中有 Properties/launchSettings.json 文件,则应用程序的 HTTP 端口和环境将从该文件读取。


理解 Startup 类

Program类负责快速启动应用程序,但最重要的配置工作是委托给UseStartup方法实现的,如下所示:

.UseStartup<Startup>()

UseStartup方法依赖于类型参数来标识将要配置 ASP.NET Core 的类。这个类的常规名称是Startup,这是 ASP.NET Core MVC 项目模板使用的名称,包括用于为本章创建示例项目的【空】模板。

研究Startup类是如何工作的,可以深入洞察 HTTP 请求的处理方式,以及 MVC 是如何集成到 ASP.NET Core 平台的其余部分。

在本节中,我从最简单的Startup类开始,并添加一些功能来演示不同配置选项的效果,最后得到适合于大多数 MVC 项目的配置。作为起点,清单14-12显示了 Visual Studio 为【空】项目添加的Startup类,该类设置的功能刚好足以让ASP.NET Core 处理 HTTP 请求。

清单 14-12:Startup.cs 文件的初始内容

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace ConfiguringApps {
    public class Startup {
        public void ConfigureServices(IServiceCollection services) 
        { }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env) 
        {
            if (env.IsDevelopment()) 
            {
                app.UseDeveloperExceptionPage();
            }

            app.Run(async (context) => 
            {
                await context.Response.WriteAsync("Hello World!");
            });
        }
    }
}

Startup类定义了两个方法,ConfigureServicesConfigure,它们设置了应用程序所需的共享功能,并告诉 ASP.NET Core 应该如何使用它们。

当应用程序启动时,ASP.NET Core 将创建一个新的Startup类实例,并调用其ConfigureServices方法,以便应用程序能够创建其服务。正如我在《理解 ASP.NET 服务》一节中解释的那样,服务是为应用程序的其他部分提供功能的对象。这是一个模糊的描述,但这是因为服务可以用来提供几乎任何功能。

一旦创建了服务,ASP.NET 就调用Configure方法。Configure方法的作用是设置请求管道,这是一个组件集(称为中间件),用于处理传入的 HTTP 请求并为它们生成响应。我将在《理解 ASP.NET 中间件》这一节中解释请求管道是如何工作的,并演示如何创建中间件组件。图14-3显示了 ASP.NET 使用Startup类的方式。

图14-3 ASP.NET 如何使用 Startup 类配置应用程序

对于所有请求,拥有一个只返回相同的 “Hello World!” 消息的Startup类并不特别有用,所以在详细解释类中的方法做什么之前,我需要先跳一步,启用 MVC,如清单14-13所示。

清单 14-13:ConfiguringApps 文件夹下的 Startup.cs 文件,启用 MVC

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace ConfiguringApps
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseMvcWithDefaultRoute();
        }
    }
}

有了这些新添加的内容 —— 我在后面的章节中解释 —— 就有了足够的基础设施来处理 HTTP 请求并使用控制器和视图生成响应。如果您运行应用程序,您将看到如图14-4所示的输出。

图14-4 启动 MVC 的效果

注意,内容没有添加样式。清单14-13中的最小配置不提供任何为静态内容提供服务的支持,如 CSS 样式和 JavaScript 文件,因此,Index.cshtml 视图渲染的 HTML 中的link元素会产生一个应用程序无法处理的 Bootstrap CSS 样式表的请求,它阻止浏览器获取它所需的样式信息。我在《添加剩余的中间件组件》一节中解决了这个问题。

理解 ASP.NET 服务

ASP.NET Core 通过调用Startup.ConfigureServices方法让应用程序可以设置它所需的服务。术语服务是指向应用程序的其他部分提供功能的任何对象。如前所述,这是一个模糊的描述,因为服务可以执行应用程序要求的任何事情。作为例子,我向项目添加了一个 Infrastructure 文件夹,并向其添加了一个名为 UptimeService.cs 的类文件,用于定义清单14-14所示的类。

清单 14-14:Infrastructure 文件夹下的 UptimeService.cs 文件的内容

using System.Diagnostics;

namespace ConfiguringApps.Infrastructure
{
    public class UptimeService
    {
        private Stopwatch timer;

        public UptimeService()
        {
            timer = Stopwatch.StartNew();
        }
        public long Uptime => timer.ElapsedMilliseconds;
    }
}

创建该类时,它的构造函数启动一个定时器,跟踪应用程序运行的时间。这是一个很好的服务示例,因为它提供了可以在应用程序的其余部分中使用的功能,并且在启动应用程序时可以从创建该功能中获益。

ASP.NET 服务是使用Startup类的ConfigureServices方法注册的,在清单14-15中,您可以看到我是如何注册UptimeService类的。

清单 14-15:ConfiguringApps 文件夹下的 Startup.cs 文件,注册定制服务

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using ConfiguringApps.Infrastructure;

namespace ConfiguringApps
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<UptimeService>();
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseMvcWithDefaultRoute();
        }
    }
}

ConfigureServices方法接收一个实现了IServiceCollection接口的对象作为其参数。服务是使用在IServiceCollection接口上调用的扩展方法注册的,该扩展方法指定不同的配置选项。我在第18章中描述了创建服务的可用选项,但目前我使用了AddSingleton方法,这意味着整个应用程序都将共享一个UptimeService对象。

服务与称为依赖注入的特性密切相关,该特性允许控制器等组件轻松获得服务,我在第18章中对此进行了深入描述。可以通过创建一个接受所需服务类型的参数的构造函数来访问Startup.ConfigureServices中注册的服务。清单14-16显示了我添加到 Home 控制器的构造函数,它接收我在清单14-15中创建的共享UptimeService对象。我还更新了控制器的Index action 方法,以便在它生成的视图数据中包含服务的Update属性的值。

清单 14-16:Controllers 文件夹下的 HomeController.cs 文件,访问服务

using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using ConfiguringApps.Infrastructure;

namespace ConfiguringApps.Controllers
{
    public class HomeController : Controller
    {
        private UptimeService uptime;

        public HomeController(UptimeService up) => uptime = up;

        public ViewResult Index() => View(new Dictionary<string, string>
        {
            ["Message"] = "This is the Index action",
            ["Uptime"] = $"{uptime.Uptime}ms"
        });
    }
}

当 MVC 需要 Home 控制器类的实例来处理 HTTP 请求时,它会检查HomeController构造函数并寻找它需要UptimeService对象。然后,MVC 检查已在Startup类中配置的服务集,发现UptimeService已经配置,这意味着所有请求均可使用单个UptimeService对象,并在创建HomeController时将该对象作为构造函数参数传递。

服务可以以更复杂的方式注册和使用,但这个示例演示了服务背后的核心思想,并展示了如何在Startup类中定义服务,从而允许您定义在整个应用程序中使用的功能或数据。

如果运行应用程序并请求默认的 URL,您将看到一个包含应用程序启动以来的毫秒数的响应,它是从在Startup类中创建的UptimeService对象获得的,如图14-5所示(严格地说,这个时间从创建UptimeService服务开始计算,但它距离应用程序启动已经够近了,因此对本章的目的没有任何影响)。

图14-5 使用简单服务

每次收到对默认 URL 的请求时,MVC 都会创建一个新的HomeController对象,并为其提供共享UptimeService对象作为构造函数参数。这使得 Home 控制器可以访问应用程序的运行时间,而无需关心这些信息是如何提供或实现的。

理解内置 MVC 服务

像 MVC 这样复杂的包使用许多服务;有些用于内部使用,另一些则为开发人员提供功能。程序包定义了扩展方法以在单个方法调用中设置它们所需的所有服务。对于 MVC 来说,这种方法称为AddMvc,这是我添加到Startup类中的两种方法之一,以实现 MVC 的正常运行。

...
public void ConfigureServices(IServiceCollection services) {
    services.AddSingleton<UptimeService>();
    services.AddMvc();
}
...

该方法设置 MVC 所需的每个服务,而不使用大量的单个服务列表填充ConfigureServices方法。

注意:在ConfigureServices方法中调用IServiceCollection对象时,Visual Studio 智能感知功能将向您展示一长串其他扩展方法的列表。其中一些方法,例如AddSingletonAddScoped,用于以不同的方式注册服务。其他方法如AddRoutingAddCors,添加已由AddMvc方法应用的单个服务。结果是,对于大多数应用程序,ConfigureServices方法包含少量的自定义服务、对AddMvc方法的调用,以及一些用于配置内置服务的语句,我在《配置 MVC 服务》一节中对此进行了描述。

理解 ASP.NET 中间件

在 ASP.NET Core 中中间件是一个术语,它是用于组合成请求管道的组件。请求管道像链一样排列,当一个新请求到达时,它被传递给链中的第一个中间件组件。该组件检查请求,并决定是处理请求并生成响应,还是将其传递给链中的下一个组件。一旦处理了请求,便将返回给客户端的响应沿着链原路返回,这允许所有早期组件检查或修改它。

中间件组件的工作方式似乎有点奇怪,但它允许应用程序组合的方式具有很大的灵活性。了解中间件的使用如何塑造应用程序是很重要的,特别是当您没有得到预期的响应时。为了解释中间件系统是如何工作的,我将创建一些自定义组件,演示您将要遇到的四种中间件类型中的每一种。

创建内容生成中间件

最重要的中间件类型是为客户端生成内容,而 MVC 就是属于这一类。要创建一个内容生成中间件组件,而无需 MVC 的复杂性,我在Infrastructure文件夹中添加了一个名为ContentMiddleware.cs的类,并使用它来定义清单14-17所示的类。

清单 14-17:Infrastructure 文件夹下的 ContentMiddleware.cs 文件的内容

using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;

namespace ConfiguringApps.Infrastructure
{
    public class ContentMiddleware
    {
        private RequestDelegate nextDelegate;
        public ContentMiddleware(RequestDelegate next) => nextDelegate = next;
        public async Task Invoke(HttpContext httpContext)
        {
            if (httpContext.Request.Path.ToString().ToLower() == "/middleware")
            {
                await httpContext.Response.WriteAsync(
                    "This is from the content middleware", Encoding.UTF8);
            }
            else
            {
                await nextDelegate.Invoke(httpContext);
            }
        }
    }
}

中间件组件不实现接口,也不从公共基类派生。它们定义一个构造函数并接受RequestDelegate对象然后定义Invoke方法。RequestDelegate对象表示链中的下一个中间件组件,当 ASP.NET 收到 HTTP 请求时会调用Invoke方法。

有关 HTTP 请求和将返回给客户端的响应的信息是通过传递给Invoke方法的HttpContext参数提供的。我在第17章中描述了HttpContext类及其属性,但是对于本章,只需知道清单14-17中的Invoke方法检查 HTTP 请求并检查请求是否已发送到 /middleware URL。如果有,则向客户端发送一个简单的文本响应;如果使用了不同的 URL,则将请求转发到链中的下一个组件。

请求管道是在Startup类的Configure方法中设置的。在清单14-18中,我从示例应用程序中删除了 MVC 方法,并使用ContentMiddleware类作为管道中的唯一组件。

清单 14-18:ConfiguringApps 文件夹下的 Startup.cs 文件,使用自定义中间件

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using ConfiguringApps.Infrastructure;

namespace ConfiguringApps
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<UptimeService>();
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseMiddleware<ContentMiddleware>();
        }
    }
}

自定义中间件组件在Configure方法中使用UseMiddleware扩展方法注册,使用类型参数指定中间件类。这样,ASP.NET Core 就可以建立将要使用的所有中间件组件的列表,然后实例化它们以创建链。如果运行应用程序并请求 /middleware URL,您将看到如图14-6所示的结果。

图14-6 从自定义中间件组件生成内容

图14-7说明了我使用ContentMiddleware类创建的中间件管道。当 ASP.NET Core 接收到一个 HTTP 请求时,它将它传递给在Startup类中注册的唯一中间件组件。如果 URL 为 /middleware,组件生成一个结果,返回到 ASP.NET Core 并发送到客户端。

图14-7 中间件管道示例

如果 URL 不是 /middleware,那么ContentMiddleware类将请求传递给链中的下一个组件。由于没有其他组件,所以在创建管道时,请求到达 ASP.NET Core 提供的回退(Backstop)处理程序,后者将请求沿着管道向另一个方向发送(一旦您了解其他类型的中间件是如何工作的,这个过程将更有意义)。

使用中间件中的服务

不仅仅是控制器可以使用在ConfigureServices方法中建立的服务。ASP.NET Core 检查中间件类的构造函数,并使用服务为已定义的任何参数提供值。在清单14-19中,我在ContentMiddleware类的构造函数中添加了一个参数,它告诉 ASP.NET Core 它需要一个UptimeService对象。

清单 14-19:Infrastructure 文件夹下的 ContentMiddleware.cs 文件,使用服务

using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;

namespace ConfiguringApps.Infrastructure
{
    public class ContentMiddleware
    {
        private RequestDelegate nextDelegate;
        private UptimeService uptime;

        public ContentMiddleware(RequestDelegate next, UptimeService up)
        {
            nextDelegate = next;
            uptime = up;
        }

        public async Task Invoke(HttpContext httpContext)
        {
            if (httpContext.Request.Path.ToString().ToLower() == "/middleware")
            {
                await httpContext.Response.WriteAsync(
                    "This is from the content middleware" +
                        $"(uptime: {uptime.Uptime}ms)", Encoding.UTF8);
            }
            else
            {
                await nextDelegate.Invoke(httpContext);
            }
        }
    }
}

能够使用服务意味着中间件组件可以共享公共功能并避免代码重复。运行应用程序并请求 /middleware URL,您将看到如图14-8所示的输出。

图14-8 在自定义中间件中使用服务

创建短路中间件

下一种类型的中间件在请求到达内容生成组件之前拦截请求,以短路管道进程,这通常是为了性能目的。清单14-20显示了我添加到 Infrastructure 文件夹中的一个名为 ShortCircuitMiddleware.cs 的类文件的内容。

清单 14-20:Infrastructure 文件夹下的 ShortCircuitMiddleware.cs 文件的内容

using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;

namespace ConfiguringApps.Infrastructure
{
    public class ShortCircuitMiddleware
    {
        private RequestDelegate nextDelegate;

        public ShortCircuitMiddleware(RequestDelegate next) => nextDelegate = next;

        public async Task Invoke(HttpContext httpContext)
        {
            if (httpContext.Request.Headers["User-Agent"]
                .Any(h => h.ToLower().Contains("edge")))
            {
                httpContext.Response.StatusCode = 403;
            }
            else
            {
                await nextDelegate.Invoke(httpContext);
            }
        }
    }
}

这个中间件组件检查请求的用户代理(User-Agent)首部,浏览器使用它来标识自己。使用用户代理首部来识别特定的浏览器是不够可靠的,不能在实际的应用程序中使用,但是对于本例来说是足够的。

之所以使用*短路(short-circuiting)*这个术语,是因为这种类型的中间件并不总是将请求转发到链中的下一个组件。本例中,如果用户代理首部包含术语edge,则组件将状态代码设置为 403 —— 禁用,而不将请求转发到下一个组件。由于请求被拒绝,因此没有必要让其他组件处理请求,这将导致系统资源不必要地消耗。相反,请求处理提前结束,403 响应被发送到客户端。

中间件组件按照在Startup类中的设置顺序来接收请求,这意味着必须在内容生成中间件之前设置短路中间件,如清单14-21所示。

清单 14-21:ConfiguringApps 文件夹下的 Startup.cs 文件,注册短路中间件

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using ConfiguringApps.Infrastructure;

namespace ConfiguringApps
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<UptimeService>();
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseMiddleware<ShortCircuitMiddleware>();
            app.UseMiddleware<ContentMiddleware>();
        }
    }
}

如果您运行应用程序并使用 Microsoft Edge 浏览器请求任何 URL,将看到 403 错误。来自其他浏览器的请求被 ShortCircuitMiddleware 组件忽略,并传递到链中的下一个组件,这意味着当请求的 URL 为 /middleware 时将生成响应。图14-9显示了将短路组件添加到中间件管道中。

图14-9 在自定义中间件中使用服务

创建请求编辑中间件

下一种类型的中间件组件不会生成响应。相反,它会在请求到达链中的其他组件之前更改请求。这种中间件主要用于平台集成,以丰富带有特定平台功能的 HTTP 请求的 ASP.NET Core 表示。它还可以用于准备请求,以便它们更容易被后续组件处理。作为演示,我将 BrowserTypeMiddleware.cs 文件添加到 Infrastructure 件夹中,并使用它定义了如清单14-22所示的中间件组件。

清单 14-22:Infrastructure 文件夹下的 BrowserTypeMiddleware.cs 文件的内容

using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace ConfiguringApps.Infrastructure
{
    public class BrowserTypeMiddleware
    {
        private RequestDelegate nextDelegate;

        public BrowserTypeMiddleware(RequestDelegate next) => nextDelegate = next;

        public async Task Invoke(HttpContext httpContext)
        {
            httpContext.Items["EdgeBrowser"]
                = httpContext.Request.Headers["User-Agent"]
                    .Any(v => v.ToLower().Contains("edge"));
            await nextDelegate.Invoke(httpContext);
        }
    }
}

此组件检查请求的用户代理首部并查找术语 edge,这表明请求可能是使用 Edge 浏览器发出的。HttpContext对象通过Items属性提供一个字典,用于在组件之间传递数据,并将首部搜索的结果存储在键EdgeBrowser中。

为了演示中间件组件如何协作,清单14-23展示了ShortCircuitMiddleware类,它拒绝来自 Edge 的请求,根据BrowserTypeMiddleware组件生成的数据做出决策。

清单 14-23:ShortCircuitMiddleware.cs 文件,与另一个组件合作

using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;

namespace ConfiguringApps.Infrastructure
{
    public class ShortCircuitMiddleware
    {
        private RequestDelegate nextDelegate;

        public ShortCircuitMiddleware(RequestDelegate next) => nextDelegate = next;

        public async Task Invoke(HttpContext httpContext)
        {
            if (httpContext.Items["EdgeBrowser"] as bool? == true)
            {
                httpContext.Response.StatusCode = 403;
            }
            else
            {
                await nextDelegate.Invoke(httpContext);
            }
        }
    }
}

根据它们的本质,编辑请求的中间件组件需要放在它们合作或依赖于它们所做的更改的组件之前。在清单14-24中,我已经注册了BrowserTypeMiddleware类,作为管道中的第一个组件。

清单 14-24:ConfiguringApps 文件夹下的 Startup.cs 文件,注册中间件组件

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using ConfiguringApps.Infrastructure;

namespace ConfiguringApps
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<UptimeService>();
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseMiddleware<BrowserTypeMiddleware>();
            app.UseMiddleware<ShortCircuitMiddleware>();
            app.UseMiddleware<ContentMiddleware>();
        }
    }
}

将组件放置在管道的开头,以确保在其他组件接收到请求之前已经修改了请求,如图14-10所示。

图14-10 将请求编辑组件添加到中间件管道中

创建响应编辑中间件

最后一种中间件类型对管道中其他组件生成的响应进行操作。这对于记录请求及其响应的细节或处理错误非常有用。清单14-25显示了 ErrorMiddleware.cs 文件的内容,我将该文件添加到 Infrastructure 文件夹中,以演示这种类型的中间件组件。

清单 14-25:Infrastructure 文件夹下的 ErrorMiddleware.cs 文件的内容

using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;

namespace ConfiguringApps.Infrastructure
{
    public class ErrorMiddleware
    {
        private RequestDelegate nextDelegate;

        public ErrorMiddleware(RequestDelegate next)
        {
            nextDelegate = next;
        }

        public async Task Invoke(HttpContext httpContext)
        {
            await nextDelegate.Invoke(httpContext);
            if (httpContext.Response.StatusCode == 403)
            {
                await httpContext.Response
                    .WriteAsync("Edge not supported", Encoding.UTF8);
            }
            else if (httpContext.Response.StatusCode == 404)
            {
                await httpContext.Response
                    .WriteAsync("No content middleware response", Encoding.UTF8);
            }
        }
    }
}

组件在请求通过中间件管道并生成响应之前对它不感兴趣。如果响应状态码为 403 或 404,则组件向响应添加描述性消息,所有其它响应被忽略。清单14-26显示了组件类在Startup类中的注册。

提示:您可能想知道【404 - Not Found】的状态代码来自何处,因为它不是由我创建的三个中间件组件中的任何一个设置的。答案就是当请求进入管道时,响应是如何由 ASP.NET 配置的,如果没有中间件组件更改响应,则返回给客户端。

清单 14-26:Startup.cs 文件,注册响应编辑中间件组件

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using ConfiguringApps.Infrastructure;

namespace ConfiguringApps
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<UptimeService>();
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseMiddleware<ErrorMiddleware>();
            app.UseMiddleware<BrowserTypeMiddleware>();
            app.UseMiddleware<ShortCircuitMiddleware>();
            app.UseMiddleware<ContentMiddleware>();
        }
    }
}

我注册了ErrorMiddleware类,这样它就占据了管道中的第一个位置。对于一个只对响应感兴趣的组件来说,这看起来很奇怪,但是,在链的开头注册组件可以确保它能够检查任何其他组件生成的响应,如图14-11所示。如果该组件放置在管道的靠后位置,则只能检查部分组件生成的响应。

图14-11 将响应编辑组件添加到中间件管道中

您可以通过启动应用程序并请求任何 URL(除了/medileware)来看到新中间件的效果。结果将是如图14-12所示的错误消息。

图14-12 编辑其他中间件组件的响应

理解 Configure 方法如何被调用的

ASP.NET Core 平台在Configure方法被调用之前检查它,并获取其参数列表,它使用ConfigureServices方法中设置的服务或使用表14-5所示的特殊服务提供参数。

表 14-5:可用作Configure方法参数的特殊服务

类型 描述
IApplicationBuilder 此接口定义了设置应用程序的中间件管道所需的功能。
IHostingEnvironment 该接口定义了区分不同类型环境(如开发和生产)所需的功能。

使用应用程序生成器

尽管您根本不必为Configure方法定义任何参数,但大多数Startup类将至少使用IApplicationBuilder接口,因为它允许创建中间件管道,如本章前面所示。对于自定义中间件组件,使用UseMiddleware扩展方法注册类。复杂的内容生成中间件包提供了一个方法,该方法可以在一个步骤中设置所有中间件组件,就像它们为定义所使用的服务提供了一个单一的方法一样。在 MVC 中,可以使用两种扩展方法,如表14-6所述。

表 14-6:MVC IApplicationBuilder 扩展方法

名称 描述
UseMvcWithDefaultRoute 此方法使用默认路由设置 MVC 中间件组件
UseMvc 此方法使用 lambda 表达式指定的自定义路由配置来设置 MVC 中间件组件

路由是将请求 URL 映射到应用程序定义的控制器以及 action 的过程;我在第15章和第16章中详细描述了路由。UseMvcWithDefaultRoute方法对于开始 MVC 开发非常有用,但是大多数应用程序还是调用UseMvc方法,即使它与显式定义由UseMvcWithDefaultRoute方法创建的路由配置结果相同,如清单14-27所示。因为这使得其他开发人员和容易读懂应用程序使用的路由配置,并使以后添加新路由变得容易(几乎所有应用程序都需要在某个时候这样做)。

清单 14-27:ConfiguringApps 文件夹下的 Startup.cs 文件,设置 MVC 中间件

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using ConfiguringApps.Infrastructure;

namespace ConfiguringApps
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<UptimeService>();
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseMiddleware<ErrorMiddleware>();
            app.UseMiddleware<BrowserTypeMiddleware>();
            app.UseMiddleware<ShortCircuitMiddleware>();
            app.UseMiddleware<ContentMiddleware>();

            app.UseMvc(routes => {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

因为 MVC 设置了内容生成间件组件,所以在注册了所有其他中间件组件之后再调用UseMvc方法。要准备 MVC 所依赖的服务,必须在ConfigureServices方法中调用AddMvc方法。

使用宿主环境

表14-7中描述了IHostingEnvironment接口的属性提供的一些关于应用程序正在运行的宿主环境的基本但很重要的信息。

表 14-7:IHostingEnvironment 的属性

名称 描述
ApplicationName 此属性返回由宿主平台设置的应用程序名称。
EnvironmentName 此属性返回描述当前环境的字符串,如此表后面所述。
ContentRootPath 此属性返回包含应用程序的内容文件和配置文件的路径。
WebRootPath 此属性返回一个字符串,该字符串指定包含应用程序静态内容的目录。通常是 wwwroot 文件夹。
ContentRootFileProvider 此属性返回一个实现IFileProvider接口的对象,该对象可用于从ContentRootPath属性指定的文件夹中读取文件。
WebRootFileProvider 此属性返回一个实现IFileProvider接口的对象,该对象可用于从WebRootPath属性指定的文件夹读取文件。

ContentRootPathWebRootPath属性在大多数应用程序中都很有趣,但并不需要,因为有一个内置中间件组件可用于交付静态内容,如本章后面的《启用静态内容》一节所述。

重要的属性是EnvironmentName,它允许根据运行应用程序的环境修改应用程序的配置。有三个常规环境(开发、暂存和生产),每个都代表一个常用的环境。

当前的托管环境是使用一个名为ASPNETCORE_ENVIRONMENT的环境变量来设置的。要设置环境变量,请从 Visual Studio 【项目】菜单中选择【ConfiguringApps 属性】,然后切换到调试选项卡。双击【环境变量】的【值】区域,它默认被设置为Development,请更改为Staging,如图14-13所示。保存更改以使新的环境名称生效。

提示:环境名称不区分大小写的,因此Stagingstaging被视为相同的环境。虽然developmentstagingproduction是约定的环境,但您可以使用任何您喜欢的名称。如果一个项目中有多个开发人员,并且每个开发人员都需要不同的配置设置,这可能很有用。请参阅本章后面的《处理复杂配置》一节,以了解如何处理环境配置之间复杂差异的详细信息。

图14-13 设置宿主环境的名称

Configure方法中,可以通过读取IHostingEnvironment.EnvironmentName属性或使用在IHostingEnvironment对象上操作的扩展方法之一来确定正在使用的宿主环境,如表14-8所述。

表 14-8:IHostingEnvironment 扩展方法

名称 描述
IsDevelopment() 当宿主环境名称为Development时,此方法返回true
IsStaging() 当宿主环境名称为Staging时,此方法返回true
IsProduction() 当宿主环境名称为Production时,此方法返回true
IsEnvironment(env) 当宿主环境名称为与env参数匹配时,此方法返回true

扩展方法用于修改管道中的中间件组件集,以使应用程序的行为适合不同的宿主环境。在清单14-28中,我使用一种扩展方法来确保本章前面创建的定制中间件组件仅存在于Development宿主环境的管道中。

清单 14-28:ConfiguringApps 文件夹下的 Startup.cs 文件,使用宿主环境

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using ConfiguringApps.Infrastructure;

namespace ConfiguringApps
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<UptimeService>();
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseMiddleware<ErrorMiddleware>();
                app.UseMiddleware<BrowserTypeMiddleware>();
                app.UseMiddleware<ShortCircuitMiddleware>();
                app.UseMiddleware<ContentMiddleware>();
            }

            app.UseMvc(routes => {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

使用当前的配置,这三个自定义中间件组件将不会添加到管道中,后者已将宿主环境设置为Staging。如果您运行应用程序并请求 /middleware URL,您将收到一个【404 - Not Found】的错误,因为唯一可用的中间件组件是UseMvc方法设置的那些组件,它没有可用的控制器来处理这个 URL。

注意:一旦测试了更改宿主环境的效果,请确保将其更改为Development环境;否则,本章其余部分中的示例将无法正常工作。

添加其余的中间件组件

这里有一组常用的中间件组件,它们在大多数 MVC 项目中都很有用,我在本书中的示例中使用了这些组件。在接下来的部分中,我将这些组件添加到请求管道中,并解释它们是如何工作的。

添加异常处理

即使是编写得最仔细的应用程序也会遇到异常,重要的是适当地处理它们。在清单14-29中,我将处理异常的中间件组件添加到Startup类的请求管道中,我还删除了定制的中间件组件,以便重点关注 MVC。

清单 14-29:ConfiguringApps 文件夹下的 Startup.cs 文件,添加异常处理中间件

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using ConfiguringApps.Infrastructure;

namespace ConfiguringApps
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<UptimeService>();
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseStatusCodePages();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            app.UseMvc(routes => {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

UseStatusCodePages方法将描述性消息添加到不包含任何内容的响应中,例如【404-Not Found】响应,这可能很有用,因为并非所有浏览器都向用户显示自己的消息。

UseDeveloperExceptionPage方法设置一个错误处理中间件组件,该组件在响应中显示异常的详细信息,包括异常跟踪。这不是应该向用户显示的信息,因此,对UseDeveloperExceptionPage的调用仅在开发宿主环境中进行,该环境使用IHostingEnvironmment对象检测到。

对于暂存或生产环境,使用UseExceptionHandler方法代替。该方法设置一个错误处理,允许显示一个自定义错误消息,从而不会泄露应用程序的内部工作。UseExceptionHandler方法的参数是应该重定向的 URL,以便接收错误消息。这可以是应用程序提供的任何 URL,但约定是使用 /Home/Error。

在清单14-30中,我将根据需要生成异常的能力添加到 Home 控制器的Index action 中,并添加了一个Error action,以便可以处理UseExceptionHandler组件生成的请求。

清单 14-30:Controllers 文件夹下的 HomeController.cs 文件,生成并处理异常

using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using ConfiguringApps.Infrastructure;

namespace ConfiguringApps.Controllers
{
    public class HomeController : Controller
    {
        private UptimeService uptime;

        public HomeController(UptimeService up) => uptime = up;

        public ViewResult Index(bool throwException = false)
        {
            if (throwException)
            {
                throw new System.NullReferenceException();
            }
            return View(new Dictionary<string, string>
            {
                ["Message"] = "This is the Index action",
                ["Uptime"] = $"{uptime.Uptime}ms"
            });
        }
        public ViewResult Error() => View(nameof(Index),
            new Dictionary<string, string>
            {
                ["Message"] = "This is the Error action"
            });
    }
}

Index action 的更改依赖于我在第26章中描述的模型绑定功能,以便从请求中获得一个throwException值。如果throwExceptiontrue,则该 action 抛出一个NullReferenceException异常,如果为false,则正常执行。

Error action 使用 Index 视图显示一个简单的消息。通过运行应用程序并请求 /Home/Index?throwException=true URL,您可以看到不同的异常处理中间件组件的效果。查询字符串提供Index action 参数的值,您看到的响应将取决于宿主环境名称。图14-14显示了UseDeveloperExceptionPage(用于开发宿主环境)和UseExceptionHandler中间件(用于所有其他宿主环境)产生的输出。

图14-14 在开发和 暂存/生产 环境中处理异常

开发者异常页提供了异常的详细信息和检查其堆栈跟踪的选项以及导致异常的请求。相反,应该使用用户异常页来简单表明出了问题。

启用浏览器链接

我在第6章中描述了浏览器链接功能,并演示了如何在开发期间使用它来管理浏览器。浏览器链接的服务器端部分是作为中间件组件实现的,必须将其作为应用程序配置的一部分添加到Startup类中,如未集成到 Visual Studio 中将无法工作。浏览器链接仅在开发期间有用,不应用于暂存或生产,因为它编辑其他中间件组件生成的响应以插入 JavaScript 代码,打开 HTTP 连接重回服务器端,以便接收重新加载通知。在清单14-31中,您可以看到注册中间件组件的UseBrowserLink方法是如何只为开发宿主环境调用的。

清单 14-31:ConfiguringApps 文件夹下的 Startup.cs 文件,启用浏览器链接

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using ConfiguringApps.Infrastructure;

namespace ConfiguringApps
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<UptimeService>();
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseStatusCodePages();
                app.UseBrowserLink();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            app.UseMvc(routes => {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

译者注: 从 .NET Core 2.0 更换到 .NET Core 2.1 后,发现已经不支持app.UseBrowserLink(),查官方文档,发现 .NET Core 2.1 中需要安装Microsoft.VisualStudio.Web.BrowserLink NuGet 程序包方可使用此项功能。

启用静态内容

对大多数项目有用的最后一个中间件组件提供对 wwwroot 文件夹中的文件的访问,以便应用程序可以包括图像、JavaScript 文件和 CSS 样式表。UseStaticFiles方法添加了一个组件,用于短路静态文件的请求管道,如清单14-32所示。

清单 14-32:ConfiguringApps 文件夹下的 Startup.cs 文件,启用静态内容

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using ConfiguringApps.Infrastructure;

namespace ConfiguringApps
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<UptimeService>();
            services.AddMvc();
        }
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseStatusCodePages();
                app.UseBrowserLink();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }
            app.UseStaticFiles();
            app.UseMvc(routes => {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

无论宿主环境如何,静态内容都是必需的,这就是为什么我为所有环境调用UseStaticFiles的原因。这意味着 Index 视图中的link元素将正常工作,并允许浏览器加载 Bootstrap CSS 样式页。您可以通过启动应用程序看到效果,如图14-15所示。

图14-15 启用静态内容

配置应用程序

一些配置数据经常会发生更改,例如当应用程序从开发环境迁移到生产环境时,数据库服务器需要不同的详细信息。与在Startup类中对此信息进行硬编码不同,ASP.NET Core 允许从一系列更容易更改的源(如环境变量、命令行参数和以 JavaScript 对象表示法(Json)格式编写的文件)提供配置数据。

配置数据通常是自动处理的,但是由于我已经替换了Program类中的默认设置,所以需要显式地添加将获取数据的代码,并将其用于应用程序的其余部分,如清单14-33所示。

清单 14-33:ConfiguringApps 文件夹下的 Program.cs 文件,载入配置数据

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System.Reflection;

namespace ConfiguringApps
{
    public class Program
    {
        public static void Main(string[] args)
        {
            BuildWebHost(args).Run();
        }
        public static IWebHost BuildWebHost(string[] args)
        {
            return new WebHostBuilder()
                .UseKestrel()
                .UseContentRoot(Directory.GetCurrentDirectory())
                .ConfigureAppConfiguration((hostingContext, config) => {
                    config.AddJsonFile("appsettings.json",
                        optional: true, reloadOnChange: true);
                    config.AddEnvironmentVariables();
                    if (args != null)
                    {
                        config.AddCommandLine(args);
                    }
                })
                .UseIISIntegration()
                .UseStartup<Startup>()
                .Build();
        }
    }
}

ConfigureAppConfiguration方法用于处理配置数据,其参数是一个WebHostBuilderContext对象和一个实现IConfigurationBuilder接口的对象。WebBostBuilderContext类定义了表14-9中描述的属性。

表 14-9:由 WebBostBuilderContext 类定义的属性

名称 描述
HostingEnvironment 此属性返回一个实现IHostingEnvironment接口的对象,并提供有关运行应用程序的宿主环境的信息。有关详细信息,请参阅本章前面的《使用宿主环境》一节。
Configuration 此属性返回一个实现IConfiguration接口的对象,该对象提供对应用程序中配置数据的只读访问。

IConfigurationBuilder接口用于为应用程序的其余部分准备配置数据,这通常是使用扩展方法完成的。清单14-33中用于添加配置数据的三个方法在表14-10中进行了描述。

表 14-10:用于添加配置数据的IConfigurationBuilder扩展方法

名称 描述
AddJsonFile 此方法用于从 JSON 文件加载配置数据,如 appsettings.json。
AddEnvironmentVariables 此方法用于从环境变量加载配置数据。
AddCommandLine 此方法用于从启动应用程序的命令行参数加载配置数据。

在用于加载清单14-33中的配置数据的三种方法中,最有趣的是AddJsonFile方法。该方法的参数指定文件名,该文件是否为可选文件,以及如果文件更改是否应重新加载配置数据:

...
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
...

参数值指定了一个名为 appsettings.json 的文件,它是 JSON 配置文件的传统名称。此文件是可选的,意味着如果该文件不存在,将不会引发异常,并将监视更改,以便能够自动刷新配置数据。


重新加载配置数据

ASP.NET Core 配置系统支持在配置文件更改时重新加载数据。一些内置的中间件组件,如日志记录系统支持此功能,这意味着可以在运行时更改日志级别,而无需重新启动应用程序。您也可以在自定义中间件组件中集成类似的功能。

但是仅仅因为一个功能使某些事情成为可能并不意味着它是明智的。对生产系统上的配置文件进行更改会导致停机,错误输入的更改并创建导致故障的配置是很容易的。即使您成功地进行了更改,也可能出现不可预见的后果,例如记录数据充满磁盘或降低性能。

我的建议是避免实时编辑,并确保在部署到生产之前,所有更改都经过标准测试过程。在实时系统周围窥探以诊断一个问题很吸引人,但它很少有好的结果。如果您发现自己正在编辑生产配置文件,那么您应该问问自己是否要将一个小问题变成一个大得多的问题。


创建 JSON 配置文件

appsettings.json 文件最常见的用途是存储数据库连接字符串和日志设置,但可以存储应用程序所需的任何数据。

要了解配置系统是如何工作的,请将名为 appsettings.json 的新 JSON 文件添加到项目的根文件夹中,内容如清单14-34所示。

清单 14-34:ConfiguringApps 文件夹下的 appsettings.json 文件的内容

{
  "ShortCircuitMiddleware": {
    "EnableBrowserShortCircuit": true
  }
}

JSON 格式允许为配置设置定义结构。清单中的 JSON 内容定义了一个名为ShortCircuitMiddleware的配置类别,它包含一个名为EnableBrowserShortCircuit的配置属性,该属性被设置为true


JSON:引号和逗号

如果您刚开始使用 JSON,那么需要花一些时间在www.json.org上阅读规范。这种格式很容易使用,并且在大多数平台上都有很好的支持来生成和解析 JSON 数据,包括在 MVC 应用程序中(参见第20和21章获取示例),以及在客户机上使用一个简单的 JavaScript API。事实上,大多数 MVC 开发人员根本不会直接处理 JSON,只需要在配置文件中手工制作 JSON。

许多刚接触 JSON 的开发人员都遇到了两个缺陷,虽然您仍然应该花时间阅读规范,但是当 Visual Studio 或 ASP.NET Core 无法解析您的 JSON 文件时,知道最常见的问题将使得您知道从哪开始。下面是添加至 appsettings.json 文件的内容,以演示两个最常见的问题:

{
    "ShortCircuitMiddleware": {
        "EnableBrowserShortCircuit": true
    }
    mysetting : [ fast, slow ]
}

首先,JSON 中的几乎所有内容都要用引号括起来。这很容易被忘记,您以为正在编写 C# 代码,并且期望没有引号的属性名称和值被接受。在 JSON 中,除布尔值和数字以外的任何内容都需要用引号括起来,如下:

{
    "ShortCircuitMiddleware": {
        "EnableBrowserShortCircuit": true
    }
    "mysetting" : [ "fast", "slow"]
}

其次,当您向对象的 JSON 描述中添加新属性时,必须记住在前面的大括号字符后添加逗号,如下所示:

{
"ShortCircuitMiddleware": {
        "EnableBrowserShortCircuit": true
    },
    "mysetting" : [ "fast", "slow"]
}

即使在高亮显示时也很难看出两者之间的差异 —— 这就是此错误如此常见的原因 —— 我在的}字符后面添加了一个逗号以结束ShortCircuitMiddleware部分。但是要小心,如果没有后续部分,添加后缀逗号也是非法的。如果您的 JSON 更改导致了问题,那么首先检查这两个错误。


使用配置数据

Startup类可以通过定义带有IConfiguration参数的构造函数来访问配置数据。当在Program类中调用UseStartup方法时,ConfigureApp 所准备的配置数据将用于创建Startup对象。清单14-35显示了将构造函数添加到Startup类中,并展示了如何访问配置数据。

清单 14-35:ConfiguringApps 文件夹下的 Startup.cs 文件,接收并使用配置数据

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using ConfiguringApps.Infrastructure;
using Microsoft.Extensions.Configuration;

namespace ConfiguringApps
{
    public class Startup
    {
        public IConfiguration Configuration { get; }

        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<UptimeService>();
            services.AddMvc();
        }
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if ((Configuration.GetSection("ShortCircuitMiddleware")?
                .GetValue<bool>("EnableBrowserShortCircuit")).Value)
            {
                app.UseMiddleware<BrowserTypeMiddleware>();
                app.UseMiddleware<ShortCircuitMiddleware>();
            }

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseStatusCodePages();
                app.UseBrowserLink();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }
            app.UseStaticFiles();
            app.UseMvc(routes => {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

IConfiguration对象由构造函数接收并分配给一个名为Configuration的属性,该属性随后可用于访问从环境变量、命令行和appsettings.json文件加载的配置数据。

要获得一个值,您可以通过数据的结构导航到所需的配置部分,它由另一个实现了IConfiguration接口的对象表示,它为IConfigurationRoot提供了一个可用的成员子集,如表14-11所示。

表 14-11:IConfiguration 接口定义的成员

名称 描述
[key] 此索引器用于获取指定键的字符串值。
GetSection(name) 此方法返回表示配置数据的一部分的IConfiguration对象。
GetChildren() 此方法返回表示当前配置对象的子部分的IConfiguration对象的枚举。

还有一些扩展方法可以用于对IConfiguration对象进行操作,以获取值并将它们从字符串转换为其他类型,如表14-12所述。

表 14-12:IConfiguration 接口的扩展方法

名称 描述
GetValue(keyName) 此方法获取与指定键关联的值,并尝试将其转换为T类型。
GetValue(keyName, defaultValue) 此方法获取与指定键关联的值,并尝试将其转换为T类型。如果配置数据中没有键的值,则将使用默认值。

重要的是不要假设将指定配置值。在清单中,我使用空条件运算符来确保在尝试获取EnableBrowserShortCircuit值之前已经收到了ShortCircuitMiddleware部分。结果是,只有在定义了ShortCircuitMiddleware/EnableBrowserShortCircuit值并将其设置为true时,才会将自定义中间件添加到请求管道中。

配置日志

ASP.NET Core 支持捕获和处理日志数据,许多内置中间件组件都是为了生成日志消息而编写的。日志是在大多数项目中自动设置的,但是由于我在Program类中使用单个配置语句,因此需要添加清单14-36中所示的语句来设置日志功能。

清单 14-36:ConfiguringApps 文件夹下的 Program.cs 文件,配置日志

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System.Reflection;

namespace ConfiguringApps
{
    public class Program
    {
        public static void Main(string[] args)
        {
            BuildWebHost(args).Run();
        }
        public static IWebHost BuildWebHost(string[] args)
        {
            return new WebHostBuilder()
                .UseKestrel()
                .UseContentRoot(Directory.GetCurrentDirectory())
                .ConfigureAppConfiguration((hostingContext, config) => {
                    config.AddJsonFile("appsettings.json",
                        optional: true, reloadOnChange: true);
                    config.AddEnvironmentVariables();
                    if (args != null)
                    {
                        config.AddCommandLine(args);
                    }
                })
                .ConfigureLogging((hostingContext, logging) => {
                    logging.AddConfiguration(
                    hostingContext.Configuration.GetSection("Logging"));
                    logging.AddConsole();
                    logging.AddDebug();
                })
                .UseIISIntegration()
                .UseStartup<Startup>()
                .Build();
        }
    }
}

ConfigureLogging方法使用一个 lambda 函数来设置日志系统,该函数接收一个WebHostBuilderContext对象(在本章前面描述)和一个实现ILoggingBuilder接口的对象。一组扩展方法在ILoggingBuilder接口上操作,以配置日志记录系统,如表14-13所述。

** 表 14-13**:ILoggingBuilder 接口的扩展方法

名称 描述
AddConfiguration 此方法用于使用从 appsettings.json 文件、命令行或环境变量中加载的配置数据配置日志系统。
AddConsole 此方法将日志消息发送到控制台,这在使用dotnet run命令启动应用程序时非常有用。
AddDebug 此方法在 Visual Studio 调试器运行时将日志记录消息发送到调试输出窗口。
AddEventLog 此方法将日志记录消息发送到 Windows 事件日志,如果您部署到 Windows 服务器,并希望将来自 ASP.NET Core MVC 应用程序的日志消息与其他类型的应用程序的日志消息结合起来,则此方法非常有用。

理解日志配置数据

AddConfiguration方法使用通常在 appsettings.json 文件中定义的配置数据来配置日志系统。清单14-37在 appsettings.json 文件中添加了一个名为Logging的配置部分,它对应于清单14-36中的AddConfiguration方法的名称。

清单 14-37:ConfiguringApps 文件夹下的 appsettings.json 文件,添加配置部分

{
  "ShortCircuitMiddleware": {
    "EnableBrowserShortCircuit": true
  },
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  }
}

日志配置指定了应该从不同的日志数据源显示的消息级别。日志系统支持六种级别的调试信息,如表14-14所述,按重要性排序。

表 14-14:ASP.NET 调试等级

等级 描述
Trace 此级别用于在开发过程中有用但在生产中不需要的消息。
Debug 此级别用于开发人员调试问题所需的详细消息。
Information 此级别用于描述应用程序的一般操作的消息。
Warning 此级别用于描述意外但不中断应用程序的事件的消息。
Error 此级别用于描述中断应用程序的错误的消息。
Critical 此级别用于描述灾难性故障的消息。
None 此级别用于禁用日志消息。

清单14-37中的默认条目设置了显示 Debug 日志消息的阈值,这意味着只显示 Debug 级别或更高级别的消息。其余条目覆盖来自特定命名空间的默认日志消息,以便只有在 Information 级别或更高级别时才会显示源自SystemMicrosoft命名空间的日志消息。

若要查看启用日志的效果,请选择【调试】➤【开始调试】,使用 Visual Studio 调试器启动应用程序。查看输出窗口,您将看到显示每个 HTTP 请求是如何处理的日志消息,如下所示:

info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
      Request starting HTTP/1.1 GET http://localhost:65417/
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
      Executing action method ConfiguringApps.Controllers.HomeController.Index
      (ConfiguringApps) with arguments (False) - ModelState is Valid
info: Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.ViewResultExecutor[1]
      Executing ViewResult, running view at path /Views/Home/Index.cshtml.
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
      Executed action ConfiguringApps.Controllers.HomeController.Index
      (ConfiguringApps) in 1597.3535ms
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
      Request finished in 1695.6314ms 200 text/html; charset=utf-8

创建自定义日志消息

上一节中的日志消息是由 ASP.NET Core 和 MVC 组件生成的,它们处理 HTTP 请求并生成响应。这种消息可以提供有用的信息,但也可以生成特定于应用程序的自定义日志消息,如清单14-38所示。

清单 14-38:Controllers 文件夹下的 HomeController.cs 文件,自定义日志

using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using ConfiguringApps.Infrastructure;
using Microsoft.Extensions.Logging;

namespace ConfiguringApps.Controllers
{
    public class HomeController : Controller
    {
        private UptimeService uptime;

        private ILogger<HomeController> logger;
        public HomeController(UptimeService up, ILogger<HomeController> log)
        {
            uptime = up;
            logger = log;
        }

        public ViewResult Index(bool throwException = false)
        {
            logger.LogDebug($"Handled {Request.Path} at uptime {uptime.Uptime}");

            if (throwException)
            {
                throw new System.NullReferenceException();
            }
            return View(new Dictionary<string, string>
            {
                ["Message"] = "This is the Index action",
                ["Uptime"] = $"{uptime.Uptime}ms"
            });
        }
        public ViewResult Error() => View(nameof(Index),
            new Dictionary<string, string>
            {
                ["Message"] = "This is the Error action"
            });
    }
}

ILogger接口定义了创建日志条目和获取实现该接口的对象所需的功能,HomeController类有一个构造函数参数,其类型为ILogger<HomeController>。类型参数允许日志系统在日志消息中使用类的名称,构造函数参数的值是通过我在第18章中描述的依赖项注入特性自动提供的。

一旦有了ILogger,就可以使用Microsoft.Extensions.Logging命名空间中定义的扩展方法来创建日志消息。表14-14描述了每个日志级别的方法。HomeController类使用LogDebug方法在 Debug 级别创建消息。要查看效果,请使用 Visual Studio 调试器运行应用程序,并检查日志消息的输出窗口,如下所示:

dbug: ConfiguringApps.Controllers.HomeController[0]
  Handled / at uptime 12

当应用程序启动时,会显示大量的消息,这会使选择单个消息变得困难。如果单击【输出】窗口顶部的【全部清除】按钮,然后重新加载浏览器,则更容易查看单个消息 —— 这将确保只显示与单个请求相关的日志消息。

配置依赖注入

ASP.NET Core 应用程序的默认配置包括准备服务提供者,这是我在第18章中详细描述的依赖项注入特性所使用的。清单14-39演示了将配置语句添加到Program类中。

清单 14-39:ConfiguringApps 文件夹下的 Program.cs 文件,配置服务

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System.Reflection;

namespace ConfiguringApps
{
    public class Program
    {
        public static void Main(string[] args)
        {
            BuildWebHost(args).Run();
        }
        public static IWebHost BuildWebHost(string[] args)
        {
            return new WebHostBuilder()
                .UseKestrel()
                .UseContentRoot(Directory.GetCurrentDirectory())
                .ConfigureAppConfiguration((hostingContext, config) => {
                    config.AddJsonFile("appsettings.json",
                        optional: true, reloadOnChange: true);
                    config.AddEnvironmentVariables();
                    if (args != null)
                    {
                        config.AddCommandLine(args);
                    }
                })
                .ConfigureLogging((hostingContext, logging) => {
                    logging.AddConfiguration(
                    hostingContext.Configuration.GetSection("Logging"));
                    logging.AddConsole();
                    logging.AddDebug();
                })
                .UseIISIntegration()
                .UseDefaultServiceProvider((context, options) => {
                    options.ValidateScopes =
                        context.HostingEnvironment.IsDevelopment();
                })
                .UseStartup<Startup>()
                .Build();
        }
    }
}

UseDefaultServiceProvider方法使用内置的 ASP.NET Core 服务提供程序。有可供选择的服务提供者,但是内建功能对于大多数项目来说是可以接受的,我建议您只在需要解决特定问题并且对依赖注入有很好的理解的情况下才使用第三方组件,我将在第18章中对此进行描述。

UseDefaultServiceProvider接受一个 lambda 函数,该函数接收WebHostBuilderContext对象和ServiceProviderOptions对象,它用于配置内置服务提供程序。唯一的配置属性称为ValidateScopes,在使用 Entity Framework Core 时需要禁用该特性,如第8章所述。

配置 MVC 服务

Startup类的ConfigureServices方法中调用AddMvc时,它将设置 MVC 应用程序所需的所有服务。它非常方便,因为在一个步骤中注册了所有 MVC 服务,但确实意味着需要做一些额外的工作来重新配置服务以更改默认行为。

AddMvc方法返回一个实现IMvcBuilder接口的对象,MVC 提供了一组可用于高级配置的扩展方法,其中最有用的方法见表14-15。这些配置选项中有许多与我在后面章节中详细描述的特性有关。

表 14-15:常用 IMvcBuilder 扩展方法

名称 描述
AddMvcOptions 此方法配置 MVC 使用的服务,如表后面所述。
AddFormatterMappings 此方法用于配置允许客户端指定其接收的数据格式的特性,如第20章所述。
AddJsonOptions 该方法用于配置创建 JSON 数据的方式,如第20章所述。
AddRazorOptions 该方法用于配置 Razor 视图引擎,如第21章所述。
AddViewOptions 此方法用于配置 MVC 处理视图的方式,包括使用哪个视图引擎。详情请参见第21章。

AddMvcOptions方法配置最重要的 MVC 服务。它接受一个接收MvcOptions对象的函数,该对象提供一组配置属性,其中最有用的配置属性见表14-16。

表 14-16:选择 MvcOptions 属性

名称 描述
Conventions 此属性返回用于自定义 MVC 如何创建控制器和 action 的模型约定列表,如第31章所述。
Filters 此属性返回全局筛选器的列表,如第19章所述。
FormatterMappings 此属性返回用于允许客户端指定其接收的数据格式的映射,如第20章所述。
InputFormatters 此属性返回用于解析请求数据的对象列表,如第20章所述。
ModelValidatorProviders 此属性返回用于验证数据的对象的列表,如第27章所述。
OutputFormatters 此属性返回从 API 控制器发送的格式数据的类的列表,如第20章所述。
RespectBrowserAcceptHeader 此属性指定在决定用于响应的数据格式时是否考虑到Accept首部,如第20章所述。

这些配置选项用于微调 MVC 的操作方式,您将在表中指定的章节中找到它们所涉及的特性的详细描述。作为一个快速演示,清单14-40展示了如何使用AddMvcOptions方法更改配置选项。

清单 14-40:ConfiguringApps 文件夹下的 Startup.cs 文件,更改配置选项

...
public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<UptimeService>();
    services.AddMvc().AddMvcOptions(options => {
        options.RespectBrowserAcceptHeader = true;
    });
}
...

传递给AddMvcOptions方法的 lambda 表达式接收MvcOptions对象,我将它的RespectBrowserAcceptHeader属性设置为true。此更改允许客户端对内容协商过程所选择的数据格式产生更大的影响,如第20章所述。

处理复杂配置

如果您需要支持大量的托管环境,或者您的托管环境之间有很大的差异,那么使用if语句在Startup类中对配置进行分支,这可能导致配置难以阅读和编辑,但不会导致意外的更改。在下面的部分中,我描述了Startup类可以用于复杂配置的不同方式。

创建不同的外部配置文件

应用程序的默认配置由Program类通过查找指定用于运行应用程序的托管环境的 JSON 配置文件来执行,因此,可以使用一个名为 appsettings.production.json 的文件来存储特定于生产平台的设置。清单14-41恢复了将 JSON 文件加载到Program类的语句,我在本章的开头删除了该语句。

清单 14-41:ConfiguringApps 文件夹下的 Program.cs 文件,载入环境文件

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System.Reflection;

namespace ConfiguringApps
{
    public class Program
    {
        public static void Main(string[] args)
        {
            BuildWebHost(args).Run();
        }
        public static IWebHost BuildWebHost(string[] args)
        {
            return new WebHostBuilder()
                .UseKestrel()
                .UseContentRoot(Directory.GetCurrentDirectory())
                .ConfigureAppConfiguration((hostingContext, config) => {
                    var env = hostingContext.HostingEnvironment;
                    config.AddJsonFile("appsettings.json",
                        optional: true, reloadOnChange: true)
                        .AddJsonFile($"appsettings.{env.EnvironmentName}.json",
                            optional: true, reloadOnChange: true);
                    config.AddEnvironmentVariables();
                    if (args != null)
                    {
                        config.AddCommandLine(args);
                    }
                })
                .ConfigureLogging((hostingContext, logging) => {
                    logging.AddConfiguration(
                    hostingContext.Configuration.GetSection("Logging"));
                    logging.AddConsole();
                    logging.AddDebug();
                })
                .UseIISIntegration()
                .UseDefaultServiceProvider((context, options) => {
                    options.ValidateScopes =
                        context.HostingEnvironment.IsDevelopment();
                })
                .UseStartup<Startup>()
                .Build();
        }
    }
}

当您从特定于平台文件加载配置数据时,它包含的配置设置将覆盖所有具有相同名称的现有数据。例如,我使用 ASP.NET 配置文件项模板创建了一个名为 appsettings.development.json 的文件,配置数据如清单14-42所示。此文件中的配置数据将EnableBrowserShortCircuit值设置为false

提示: appsettings.development.json 文件在创建后可能会消失。如果在【解决方案资源管理器】窗口中将 appsettings.json 条目的左侧箭头展开,将看到 Visual Studio 将具有类似名称的项组合在了一起。

清单 14-42:ConfiguringApps 文件夹下的 appsettings.development.json 文件的内容

{
  "ShortCircuitMiddleware": {
    "EnableBrowserShortCircuit": false
  }
}

appsettings.json 文件将在应用程序启动时加载,然后是 appsettings.development.json 文件。其结果是,在开发环境中运行应用程序时,EnableBrowserShortCircuit值将为false,在暂存和生产环境中则为true

创建不同的配置方法

选择不同的配置数据文件可能很有用,但不能为复杂的配置提供完整的解决方案,因为数据文件不包含 C# 语句。如果希望更改用于创建服务或注册中间件组件的配置语句,则可以使用不同的方法,其中方法的名称包括宿主环境,如清单14-43所示。

清单 14-43:Starrtup.cs 文件,使用不同的方法名称

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using ConfiguringApps.Infrastructure;
using Microsoft.Extensions.Configuration;

namespace ConfiguringApps
{
    public class Startup
    {
        public IConfiguration Configuration { get; }

        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<UptimeService>();
            services.AddMvc().AddMvcOptions(options =>
            {
                options.RespectBrowserAcceptHeader = true;
            });
        }

        public void ConfigureDevelopmentServices(IServiceCollection services)
        {
            services.AddSingleton<UptimeService>();
            services.AddMvc();
        }
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseExceptionHandler("/Home/Error");
            app.UseStaticFiles();
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
        public void ConfigureDevelopment(IApplicationBuilder app,
            IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseBrowserLink();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

当 ASP.NET Core 在Startup类中查找ConfigureServicesConfigure方法时,它首先检查是否有包含宿主环境名称的方法。

在清单中,我添加了一个ConfigureDevelopmentServices方法,它将在开发环境中代替ConfigureServices方法使用,以及一个ConfigureDevelopment方法,它将代替Configure方法使用。您可以为需要支持的每个环境定义单独的方法,如果没有特定于环境的方法可用,则可以依赖调用的默认方法。在示例中,这意味着将使用ConfigureServicesConfigure方法来进行暂存和生产环境。

警告:如果定义了特定环境的方法,则不调用默认方法。例如,在清单14-43中,ASP.NET Core 不会在开发环境中调用Configure方法,因为有ConfigureDevelopment方法。这意味着每种方法都对其环境所需的完整配置负责。

创建不同的配置类

使用不同的方法意味着您不必使用if语句来检查宿主环境名称,但是它可以导致类变得很大,这本身就是一个问题。对于特别复杂的配置,最终的进展是为每个宿主环境创建一个不同的配置类。当 ASP.NET 查找Startup类时,它首先检查是否有名称包括当前托管环境的类。为此,我向项目添加了一个名为StartupDevelopment.cs的类文件,并使用它来定义清单14-44所示的类。

清单 14-44:ConfiguringApps 文件夹下的 StartupDevelopment.cs 文件的内容

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using ConfiguringApps.Infrastructure;
namespace ConfiguringApps
{
    public class StartupDevelopment
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<UptimeService>();
            services.AddMvc();
        }
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseBrowserLink();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

此类包含ConfigureServicesConfigure方法,用于特定的开发宿主环境。为了使 ASP.NET 能够找到特定环境的Startup类,需要对Program类进行更改,如清单14-45所示。

清单 14-45:Program.cs 文件,启用特定环境的 Startup

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System.Reflection;

namespace ConfiguringApps
{
    public class Program
    {
        public static void Main(string[] args)
        {
            BuildWebHost(args).Run();
        }
        public static IWebHost BuildWebHost(string[] args)
        {
            return new WebHostBuilder()
                .UseKestrel()
                .UseContentRoot(Directory.GetCurrentDirectory())
                .ConfigureAppConfiguration((hostingContext, config) => {
                    var env = hostingContext.HostingEnvironment;
                    config.AddJsonFile("appsettings.json",
                        optional: true, reloadOnChange: true)
                        .AddJsonFile($"appsettings.{env.EnvironmentName}.json",
                            optional: true, reloadOnChange: true);
                    config.AddEnvironmentVariables();
                    if (args != null)
                    {
                        config.AddCommandLine(args);
                    }
                })
                .ConfigureLogging((hostingContext, logging) => {
                    logging.AddConfiguration(
                    hostingContext.Configuration.GetSection("Logging"));
                    logging.AddConsole();
                    logging.AddDebug();
                })
                .UseIISIntegration()
                .UseDefaultServiceProvider((context, options) => {
                    options.ValidateScopes =
                        context.HostingEnvironment.IsDevelopment();
                })
                .UseStartup(nameof(ConfiguringApps))
                .Build();
        }
    }
}

UseStartup方法不是指定一个类,而是给出它应该使用的程序集的名称。当应用程序启动时,ASP.NET 将查找其名称包含宿主环境的类,如StartupDevelopmentStartupProduction,如果不存在,则返回使用常规Startup类。

总结

在本章中,我解释了如何配置 MVC 应用程序。我描述了ProgramStartup类的角色,以及它们提供的默认配置选项。我向您展示了如何使用管道处理请求,以及如何使用不同类型的中间件来控制请求流及其所引发的响应。下一章,我将介绍路由系统,它是 MVC 如何处理请求 URL 到控制器和 action 的映射。

;

© 2018 - IOT小分队文章发布系统 v0.3